package org.windowkit.toolkit.print;

import java.awt.Color;
import java.awt.Component;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsDevice;
import java.awt.GraphicsEnvironment;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.PrinterException;
import java.awt.print.PrinterJob;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.LinkedList;

import javax.print.PrintService;
import javax.print.PrintServiceLookup;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

import org.windowkit.toolkit.frame.GlassPane;
import org.windowkit.toolkit.widget.combo.DataSetComboBox;


/**
 * This class provides the frame and control for the print preview window.
 *
 * @author Eric Lundin
 */
//TODO so broked
public class PrintPreviewWindow implements
		ActionListener, ChangeListener {
	/**
	 * The maximum number of images to cache.
	 */
	private static final int MAX_CACHED_IMAGES = 10;

	/**
	 * The prefix of the fram title. In some cases the title of the window is
	 * not obtainable, i.e. the table is not displayed but is printed.
	 */
	private static final String TITLE = "Print Preview";

	/**
	 * Performs logic to generate images.
	 */
	private final AbstractPrinter printer;

	/**
	 * The graphics enviroment.
	 */
	private GraphicsEnvironment ge;

	/**
	 * The graphics device.
	 */
	private GraphicsDevice gs;

	/**
	 * The graphics configuration.
	 */
	private GraphicsConfiguration gc;

	/**
	 * The view for this window.
	 */
	protected PrintPreviewView view;

	/**
	 * Message to display current page number.
	 */
	protected MessageFormat indexMessage;

	/**
	 * Message to display while loading image
	 */
	protected MessageFormat loadMessage;

	/**
	 * The PrinterJob which will control the printing of this object.
	 */
	protected PrinterJob printJob;

	/**
	 * The current PrintService (printer) selected by the user.
	 */
	protected PrintService currentService = null;

	/**
	 * Hashmap to cache images.
	 */
	protected LimitedSizeHashMap<Integer, BufferedImage> renderedImages;

	/**
	 * The currently selected PageFormat(paper size and orientation).
	 */
	PageFormat format;

	/**
	 * Thread to run tasks which may take time.
	 */
	Thread t = null;

	/**
	 * Glass pane to display while running thread.
	 */
	GlassPane workingGlassPane;

	Component normalGlassPane;

	/**
	 * @param windowName
	 *            The name of the window. I.E. SMR List Window
	 * @param printer
	 *            The printer object which will be called to draw the pages.
	 */
	public PrintPreviewWindow(final String windowName, final PrinterJob printJob,
			final AbstractPrinter printer) {
//		super(TITLE);
		if (windowName != null) {
			//XXX
//			super.setName(TITLE + ": " + windowName);
		}
		if (printer == null) {
			throw new IllegalArgumentException("Printer cannot be null");
		}
		this.printJob = printJob;
		this.printer = printer;
		format = printJob.defaultPage();
		initView();
//		pack();
//		normalGlassPane = getGlassPane();
//		setGlassPane(workingGlassPane = new GlassPane());
		workingGlassPane.addEventForward(view.getCancelButton());
		initFunctionality();
	}

	/**
	 * Spawn a thread to initialize the functionality of this window. We use a
	 * thread since the initialization of looking up all print serivces may take
	 * a few seconds and will allow us to give a repsonsive GUI with a progress
	 * bar.
	 */
	private void initFunctionality() {
		t = new PreviewThread("Init Thread", PreviewThread.STATUS_INITIALIZING) {
			protected PrintService[] printServices;

			@Override
			public void doWork() {
				final PrintService defaultPrinter = PrintServiceLookup
						.lookupDefaultPrintService();
				ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
				gs = ge.getDefaultScreenDevice();
				gc = gs.getDefaultConfiguration();
				renderedImages = new LimitedSizeHashMap<Integer, BufferedImage>(
						MAX_CACHED_IMAGES);
				indexMessage = new MessageFormat(
						"Page: {1,number} of {2,number}");
				loadMessage = new MessageFormat("Loading Page {1,number}");
				if (!isInterrupted()) {
					printServices = PrintServiceLookup.lookupPrintServices(
							null, null);
				}
				else {
					return;
				}
				if (printServices.length > 0) {
					if (!isInterrupted()) {
						setStatusBarText(STATUS_PRINTER);
						EventQueue.invokeLater(new Runnable() {
							public void run() {
								view.populateCombobox(printServices);
								if (defaultPrinter != null) {
									view.setSelectedPrinter(defaultPrinter);
								} else {
									view.setSelectedPrinter(printServices[0]);
								}
							}
						});
					} else {
						return;
					}
				} else {

				}
			};

		};
		t.start();
	}

	/**
	 * Initialize graphic objects.
	 */
	private void initView() {
		view = new PrintPreviewView(/* printer.getTotalNumberOfPages(format,getGraphicsObject(format)) */);
		// view.setFormatDisplay(format);
//		setView(view);
	}

	/**
	 * Display the image with the given page number.
	 *
	 * @param pageNumber
	 *            The number of the page to display.
	 */
	private void displayImage(final int pageNumber) {
		BufferedImage currentImage;
		currentImage = getImage(pageNumber);
		view.setDisplayedImage(currentImage);
	}

	/**
	 * Retrieve the image representing the given printed page number. If the
	 * image has already been drawn or displayed we have it cahced and can
	 * return it. Otherwise we will render it. Display it and then store it.
	 *
	 * @param imageNumber
	 *            The number of the page whos image we wish to get.
	 * @return The image representing the given page.
	 */
	private BufferedImage getImage(final int imageNumber) {
		BufferedImage returnImage;
		if (!renderedImages.containsKey(imageNumber)) {
			returnImage = renderImage(imageNumber);
			renderedImages.put(imageNumber, returnImage);
		} else {
			returnImage = renderedImages.get(imageNumber);
		}
		return returnImage;
	}

	/**
	 * Render and return the image representing the given page.
	 *
	 * @param imageNumber
	 *            The number of the page we wish to get the image of.
	 * @return The image representing the given page.
	 */
	private BufferedImage renderImage(final int imageNumber) {
		BufferedImage returnImage;
		Graphics g2;
		int imageWidth;
		int imageHeight;
		imageWidth = (int) format.getImageableWidth();
		imageHeight = (int) format.getImageableHeight();
		returnImage = gc.createCompatibleImage(imageWidth, imageHeight);
		g2 = returnImage.createGraphics();
		Color color = g2.getColor();
		g2.setColor(Color.white);
		g2.fillRect(0, 0, imageWidth, imageHeight);
		g2.setColor(color);
		try {
			g2.setClip(0, 0, imageWidth, imageHeight);
			g2.translate((int) -format.getImageableX(), (int) -format
					.getImageableY());
			printer.print(g2, format, imageNumber - 1);
		} catch (PrinterException pe) {

		}
		return returnImage;
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see java.awt.event.ActionListener#actionPerformed(java.awt.event.ActionEvent)
	 */
	public void actionPerformed(final ActionEvent e) {
		if (e.getActionCommand().equals("Print")) {
			printComponent();
		} else if (e.getActionCommand().equals("Cancel")) {
			if (t != null && t.isAlive()) {
				((PreviewThread) t).cancelThread();
			}

		} else if (e.getActionCommand().equals("Format")) {
			determineDesiredPageFormat();
		} else if (e.getSource().equals(view.getAvailablePrintersCombo())) {
			handlePrinterChange();
		}
	}

	/**
	 * Handles the Print button being pressed. Displays a dialog passing control
	 * over to the underlying implementation of the systems print service.
	 */
	private void printComponent() {
		boolean print;
		print = printJob.printDialog();
		if (print) {
			try {

				printJob.print();
			} catch (PrinterException pe) {

			}
		}
	}

	/**
	 * Overriding begin is OK here as any user who can see the component to be
	 * printed either has read or write access and can therefore print it.
	 * Although overriding begin is somewhat of a hack fix the alternative is to
	 * make the system administrator add a right to this window for evey user.
	 */
	public boolean begin() {
		return true;
	}

	/**
	 * Handle the user's selection of a new PrintService (printer). If the
	 * service has not changed do nothing.
	 */
	private void handlePrinterChange() {
		t = new PreviewThread("Printer Change", PreviewThread.STATUS_PRINTER) {
			@Override
			public void doWork() {
				DataSetComboBox combo = view.getAvailablePrintersCombo();
				PrintService newService = null;

				if (combo.isDataSelected()) {
					newService = (PrintService) view
							.getAvailablePrintersCombo().getSelectedValue();
				}
				if (newService == null) {
					view.setFormatButtonEnabled(false);
				} else if ((currentService == null && newService != null)
						|| !currentService.equals(newService)) {
					currentService = newService;
					try {
						if (!isInterrupted()) {
							printJob.setPrintService(currentService);
						}
						if (!isInterrupted()) {
							super.setStatusBarText(STATUS_DEFAULT_FORMAT);
							setPageFormat(printJob.defaultPage());
							EventQueue.invokeLater(new Runnable() {
								public void run() {
									PrintPreviewWindow.this.view
											.setFormatButtonEnabled(true);
								}
							});
						}
					} catch (PrinterException pe) {

					}
				}
			}
		};
		t.start();
	}

	/**
	 * Display the page format dialog box to allow the user to change the page
	 * format for the given printer.
	 */
	public void determineDesiredPageFormat() {
		t = new PreviewThread("Set Page Format",
				PreviewThread.STATUS_PAGE_FORMAT) {
			@Override
			public void doWork() {
				PageFormat newFormat;
				newFormat = printJob.pageDialog(format);
				if (!isInterrupted()) {
					setPageFormat(newFormat);
				}
			}
		};
		t.start();
	}

	/**
	 * Set the page format used for printing and previewing to the given page
	 * format.
	 *
	 * @param format
	 *            The page format to use.
	 */
	public void setPageFormat(final PageFormat format) {
		this.format = format;
		view.setFormatDisplay(format);
		resetImages();
	}

	/**
	 * Clear all cached images. If the new number of pages is less than the old
	 * number of pages and the old page being viewed is out of bounds for the
	 * new page format set the currently selected page to the last page in the
	 * new format preview.
	 */
	public void resetImages() {
		renderedImages.clear();
		if (format != null) {
			int pages = printer.getTotalNumberOfPages(format,
					getGraphicsObject(format));
			view.initializePageSelector(1, pages, 1);
			displayImage(1);
		}
	}

	private Graphics2D getGraphicsObject(final PageFormat format) {
		return (Graphics2D) gc.createCompatibleImage(
				(int) format.getImageableWidth(),
				(int) format.getImageableHeight()).getGraphics();
	}

	/*
	 * (non-Javadoc)
	 *
	 * @see javax.swing.event.ChangeListener#stateChanged(javax.swing.event.ChangeEvent)
	 */
	public void stateChanged(final ChangeEvent e) {
		if (e.getSource() == view.getPageSelector()){}
			// TODO broked
//			displayImage(view.getSliderValue());
	}

	/**
	 * Extension of hashmap with the capability to limit the size of the map to
	 * allow storage of large objects without fear of memory problems.
	 *
	 * @author Eric Lundin
	 * @param <K>
	 *            The class for the keys of this map.
	 * @param <V>
	 *            The class for the values of this map.
	 */
	@SuppressWarnings("serial")
	protected class LimitedSizeHashMap<K, V> extends HashMap<K, V> {
		/**
		 * Since Java doesn't provide any reasonable way to get the size of an
		 * object we will define the maximum number of items to store. Once that
		 * limit has been reached we will start removing the components accessed
		 * the longest time ago.
		 */
		private int maxSize = 5;

		/**
		 * Will keep track of order components within this map were accessed in.
		 */
		LinkedList<K> lastAccessed;

		/**
		 * Create a new hashmap which will regulate the number of key/value
		 * pairs it holds.
		 *
		 * @param maxSize
		 *            The maximum number of key/value mappings to allow.
		 */
		public LimitedSizeHashMap(final int maxSize) {
			super();
			lastAccessed = new LinkedList<K>();
			this.maxSize = maxSize;
		}

		/**
		 * If the size of the map is more than the maximum allowed size remove
		 * the entry accessed the longest time ago.
		 */
		private void sizeMap() {
			if (size() >= maxSize) {
				super.remove(lastAccessed.poll());
			}
		}

		/**
		 * Gets the value mapped to the given key. Since we have accessed this
		 * key we move it to the end of the removal queue.
		 */
		@Override
		public V get(final Object key) {
			if (lastAccessed.contains(key)) {
				lastAccessed.remove(key);
			}
			lastAccessed.offer((K) key);
			return super.get(key);
		}

		/**
		 * Maps the given key to the given value and adds the key to the end of
		 * the removal queue. If the size of this map has become larger than the
		 * maximum allowable size we will remove the mapping which was accessed
		 * the longest time ago.
		 */
		@Override
		public V put(final K key, final V value) {
			lastAccessed.offer(key);
			sizeMap();
			return super.put(key, value);
		}
	}

	/**
	 * Abstract thread handles status bar tasks, then calls doWork() for
	 * implementing classes' task.
	 *
	 * @see PreviewThread#doWork() for information on implementing cancelable
	 *      tasks.
	 * @author Eric Lundin
	 */
	protected abstract class PreviewThread extends Thread {

		/**
		 * Status message for status bar.
		 */
		protected static final String STATUS_INITIALIZING = "Initializing";

		/**
		 * Status message for status bar.
		 */
		protected static final String STATUS_PRINTER = "Changing Printer";

		/**
		 * Status message for status bar.
		 */
		protected static final String STATUS_PAGE_FORMAT = "Setting Page Format";

		/**
		 * Status message for status bar.
		 */
		protected static final String STATUS_IDLE = "Idle";

		/**
		 * Status message for status bar.
		 */
		protected static final String STATUS_DEFAULT_FORMAT = "Setting Default Page Format";

		/**
		 * Current status bar message.
		 */
		protected String message;

		/**
		 * When thread interrupted stores whether its was an acceptable
		 * interruption. i.e. We were canceled.
		 */
		boolean interruptionOK;

		/**
		 * Create a thread with the given name and status bar message.
		 *
		 * @param threadName
		 *            The name of the thread.
		 * @param msg
		 *            The Message to display in the status bar while this thread
		 *            runs.
		 */
		public PreviewThread(final String threadName, final String msg) {
			super(threadName);
			message = msg;
			interruptionOK = false;
		}

		/**
		 * Cancel this thread safely. The implementing class must check
		 * isInterrupted and exit if it wishes to respond to cancel events.
		 */
		public void cancelThread() {
			interruptionOK = true;
			this.interrupt();
		}

		/**
		 * Sets the status bar text to be the given string. Try to use one of
		 * the constants defined in this class.
		 *
		 * @param text
		 *            The text to display on the status bar.
		 */
		public void setStatusBarText(final String text) {
			message = text;
			EventQueue.invokeLater(new Runnable() {
				public void run() {
					PrintPreviewWindow.this.view.setStatusBarText(message);
				}
			});
		}

		/**
		 * Implement the work that needs to be accomplished within this method.
		 * If the work is considered cancelable check for isInterrupted and
		 * handle accordingly. For example: This example allows current
		 * iteration of processing to be completed to prevent inderterminate
		 * object states or IO deadlocks.
		 *
		 * <pre>
		 * while (!isInterrupted()) {
		 * 	currentLine = readLine(file);
		 * 	processCurrentLine(currentLine);
		 * }
		 * </pre>
		 *
		 * Checks before performing each sub task in this task to allow the
		 * thread to be canceled. Using this pattern leads to an indeterminate
		 * object state after cancellation unless a flag is set for each
		 * subtask's completion or this entire thread is able to be and is run
		 * again before any processing occurs which relies on the state of the
		 * object being as it would after completion of the thread.
		 *
		 * <pre>
		 * try {
		 * 	if (!isInterrupted())
		 * 		doTask1();
		 * 	if (!isInterrupted())
		 * 		doTask2();
		 * 	if (!isInterrupted())
		 * 		doTask3();
		 * } catch (InterruptedException e) {
		 * 	doCleanUp();
		 * }
		 * </pre>
		 */
		public abstract void doWork();

		@Override
		public void run() {
			EventQueue.invokeLater(new Runnable() {
				public void run() {
//					PrintPreviewWindow.this.setGlassPane(workingGlassPane);
					workingGlassPane.setWaitCursorEnabled(true);
					workingGlassPane.setVisible(true);
					PrintPreviewWindow.this.view.setStatusBarActive(true);
					PrintPreviewWindow.this.view.setStatusBarText(message);

				}
			});
			doWork();
			EventQueue.invokeLater(new Runnable() {
				public void run() {
					workingGlassPane.setWaitCursorEnabled(false);
					workingGlassPane.setVisible(false);
					PrintPreviewWindow.this.view.setStatusBarActive(false);
					PrintPreviewWindow.this.view.setStatusBarText(STATUS_IDLE);
//					PrintPreviewWindow.this.setGlassPane(normalGlassPane);
				}
			});
		}
	}
}
